{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Decorators 2"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true
   },
   "source": [
    "We have seen how to create some simple and not so simple decorators.\n",
    "\n",
    "However we have also been using built-in decorators that can accept parameters, such as `wraps` and `lru_cache`.\n",
    "\n",
    "This can be quite useful and we can accomplish the same thing ourselves."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "First recall our original timer decorator from an earlier video (Decorator Application - Timer):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def timed(fn):\n",
    "    from time import perf_counter\n",
    "    \n",
    "    def inner(*args, **kwargs):\n",
    "        start = perf_counter()\n",
    "        result = fn(*args, **kwargs)\n",
    "        end = perf_counter()\n",
    "        elapsed = end - start\n",
    "        print('Run time: {0:.6f}s'.format(elapsed))\n",
    "        return result\n",
    "    \n",
    "    return inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def calc_fib_recurse(n):\n",
    "    return 1 if n < 3 else calc_fib_recurse(n-1) + calc_fib_recurse(n-2)\n",
    "\n",
    "def fib(n):\n",
    "    return calc_fib_recurse(n)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can decorate our Fibonacci function using the **@** syntax, or the longer syntax as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "fib = timed(fib)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Run time: 0.255260s\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "832040"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fib(30)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's modify this so the timer runs the function multiple times and calculates the average run time:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def timed(fn):\n",
    "    from time import perf_counter\n",
    "\n",
    "    def inner(*args, **kwargs):\n",
    "        total_elapsed = 0\n",
    "        for i in range(10):\n",
    "            start = perf_counter()\n",
    "            result = fn(*args, **kwargs)\n",
    "            end = perf_counter()\n",
    "            total_elapsed += (perf_counter() - start)\n",
    "        avg_elapsed = total_elapsed / 10\n",
    "        print('Avg Run time: {0:.6f}s'.format(avg_elapsed))\n",
    "        return result\n",
    "    \n",
    "    return inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And again we decorate it using the long syntax:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def fib(n):\n",
    "    return calc_fib_recurse(n)\n",
    "\n",
    "fib = timed(fib)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Avg Run time: 0.098860s\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "317811"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fib(28)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But that value of 10 has been hardcoded. Let's make it a parameter instead."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def timed(fn, num_reps):\n",
    "    from time import perf_counter\n",
    "    \n",
    "    def inner(*args, **kwargs):\n",
    "        total_elapsed = 0\n",
    "        for i in range(num_reps):\n",
    "            start = perf_counter()\n",
    "            result = fn(*args, **kwargs)\n",
    "            end = perf_counter()\n",
    "            total_elapsed += (perf_counter() - start)\n",
    "        avg_elapsed = total_elapsed / num_reps\n",
    "        print('Avg Run time: {0:.6f}s ({1} reps)'.format(avg_elapsed,\n",
    "                                                        num_reps))\n",
    "        return result\n",
    "    \n",
    "    return inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now to decorate our Fibonacci function we **have** to use the long syntax (as we saw in the lecture, the **@** syntax will not work):"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def fib(n):\n",
    "    return calc_fib_recurse(n)\n",
    "\n",
    "fib = timed(fib, 5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Avg Run time: 0.095708s (5 reps)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "317811"
      ]
     },
     "execution_count": 10,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fib(28)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The problem is that we cannot use the `@` decorator syntax because when using that syntax Python passes a **single** argument to the decorator: the function we are decorating - nothing else."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course we could just use what we did above, but the decorator syntax is kind of neat, so it would be nice to retain the ability to use it."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We just need to change our thinking a little bit to do this:\n",
    "\n",
    "First, when we see the following syntax:\n",
    "\n",
    "`\n",
    "@dec\n",
    "def my_func():\n",
    "    pass\n",
    "`\n",
    "\n",
    "we see that `dec` must be a function that takes a single argument, the function being decorated.\n",
    "\n",
    "You'll note that `dec` is just a function, but we do not **call** `dec` when we decorate `my_func`, we simply use the label `dec`.\n",
    "\n",
    "Then Python does:\n",
    "\n",
    "`\n",
    "my_func = dec(my_func)\n",
    "`"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's try a concrete example:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def dec(fn):\n",
    "    print (\"running dec\")\n",
    "    \n",
    "    def inner(*args, **kwargs):\n",
    "        print(\"running inner\")\n",
    "        return fn(*args, **kwargs)\n",
    "              \n",
    "    return inner"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec\n"
     ]
    }
   ],
   "source": [
    "@dec\n",
    "def my_func():\n",
    "    print('running my_func')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "As we can see, when we decorated `my_func`, the `dec` function was **called** at that time.\n",
    "\n",
    "(Because Python did this: \n",
    "\n",
    "`my_func = dec(my_func)` \n",
    "\n",
    "so `dec` was called)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And when we now call `my_func`, we see that the `inner` function is called, followed by the original `my_func`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running inner\n",
      "running my_func\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "But what if `dec` was not the decorator itself, but instead created and returned a decorator?\n",
    "\n",
    "Let's see how we might do this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def dec_factory():\n",
    "    print('running dec_factory')\n",
    "    def dec(fn):\n",
    "        print('running dec')\n",
    "        def inner(*args, **kwargs):\n",
    "            print('running inner')\n",
    "            return fn(*args, **kwargs)\n",
    "        return inner\n",
    "    return dec"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So as you can see, calling `dec_generator()` will return that `dec` function which is our decorator:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec_factory\n",
      "running dec\n"
     ]
    }
   ],
   "source": [
    "@dec_factory()\n",
    "def my_func(a, b):\n",
    "    print(a, b)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You can see that both `dec_generator` and `dec` were already called."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running inner\n",
      "10 20\n"
     ]
    }
   ],
   "source": [
    "my_func(10, 20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And there you go, all we did is basically create a decorator by calling a function (`dec_factory`) and use the return value of that call (the `dec` function) as our actual decorator."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We could have done the decoration this way too:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec_factory\n"
     ]
    }
   ],
   "source": [
    "dec = dec_factory()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec\n"
     ]
    }
   ],
   "source": [
    "@dec\n",
    "def my_func():\n",
    "    print('running my_func')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running inner\n",
      "running my_func\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Or even this way:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec_factory\n",
      "running dec\n"
     ]
    }
   ],
   "source": [
    "dec = dec_factory()\n",
    "\n",
    "def my_func():\n",
    "    print('running my_func')\n",
    "\n",
    "my_func = dec(my_func)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running inner\n",
      "running my_func\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Of course we could even decorate it this way using a single statement:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running dec_factory\n",
      "running dec\n"
     ]
    }
   ],
   "source": [
    "def my_func():\n",
    "    print('running my_func')\n",
    "\n",
    "my_func = dec_factory()(my_func)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running inner\n",
      "running my_func\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "OK, so now we have decorated our function using, not a decorator, but a decorator factory as follows:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def dec_factory():\n",
    "    def dec(fn):\n",
    "        def inner(*args, **kwargs):\n",
    "            print('running decorator inner')\n",
    "            return fn(*args, **kwargs)\n",
    "        return inner\n",
    "    return dec"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@dec_factory()\n",
    "def my_func(a, b):\n",
    "    return a + b"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running decorator inner\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "30"
      ]
     },
     "execution_count": 26,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "my_func(10, 20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "You should note that in this approach, we are **calling** `dec_factory()`, [note the parentheses `()`], and **then** using the return value (a decorator) to decorate our function."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, we could pass arguments as we do so without affecting the final outcome. In fact we can even access them from anywhere inside `dec_factory`, including any of the nested functions! \n",
    "\n",
    "Let's try this:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def dec_factory(a, b):\n",
    "    def dec(fn):\n",
    "        def inner(*args, **kwargs):\n",
    "            print('running decorator inner')\n",
    "            print('free vars: ', a, b)  # a and b are free variables!\n",
    "            return fn(*args, **kwargs)\n",
    "        return inner\n",
    "    return dec"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@dec_factory(10, 20)\n",
    "def my_func():\n",
    "    print('python rocks')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "running decorator inner\n",
      "free vars:  10 20\n",
      "python rocks\n"
     ]
    }
   ],
   "source": [
    "my_func()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "And this is how we can create decorators with parameters. We do not directly create a decorator, instead we use an outer function that returns a decorator when called, and pass arguments to that outer function, which the decorator and its inner function can of course access as nonlocal (free) variables."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So now, let's go back to our original problem where we wanted our timing decorator to run a number of loops which could be specified as a parameter when decorating the function we want to time.\n",
    "\n",
    "Here it is again:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def timed(fn, num_reps):\n",
    "    from time import perf_counter\n",
    "    \n",
    "    def inner(*args, **kwargs):\n",
    "        total_elapsed = 0\n",
    "        for i in range(num_reps):\n",
    "            start = perf_counter()\n",
    "            result = fn(*args, **kwargs)\n",
    "            end = perf_counter()\n",
    "            total_elapsed += (perf_counter() - start)\n",
    "        avg_elapsed = total_elapsed / num_reps\n",
    "        print('Avg Run time: {0:.6f}s ({1} reps)'.format(avg_elapsed,\n",
    "                                                        num_reps))\n",
    "        return result\n",
    "    \n",
    "    return inner"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "So, all we need to do is create an outer function around our timed decorator, and pass the `num_reps` argument to that outer function instead:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "def timed_factory(num_reps=1):\n",
    "    def timed(fn):\n",
    "        from time import perf_counter\n",
    "\n",
    "        def inner(*args, **kwargs):\n",
    "            total_elapsed = 0\n",
    "            for i in range(num_reps):\n",
    "                start = perf_counter()\n",
    "                result = fn(*args, **kwargs)\n",
    "                end = perf_counter()\n",
    "                total_elapsed += (perf_counter() - start)\n",
    "            avg_elapsed = total_elapsed / num_reps\n",
    "            print('Avg Run time: {0:.6f}s ({1} reps)'.format(avg_elapsed,\n",
    "                                                            num_reps))\n",
    "            return result\n",
    "        return inner\n",
    "    return timed    "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@timed_factory(5)\n",
    "def fib(n):\n",
    "    return calc_fib_recurse(n)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Avg Run time: 0.249934s (5 reps)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "832040"
      ]
     },
     "execution_count": 33,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fib(30)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Just to put the finishing touch on this, we probably don't want to have our outer function named the way it is (`timed_factory`). Instead we probably just want to call it `timed`. So lets just do this final part:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "from functools import wraps\n",
    "\n",
    "def timed(num_reps=1):\n",
    "    def decorator(fn):\n",
    "        from time import perf_counter\n",
    "\n",
    "        @wraps(fn)\n",
    "        def inner(*args, **kwargs):\n",
    "            total_elapsed = 0\n",
    "            for i in range(num_reps):\n",
    "                start = perf_counter()\n",
    "                result = fn(*args, **kwargs)\n",
    "                end = perf_counter()\n",
    "                total_elapsed += (perf_counter() - start)\n",
    "            avg_elapsed = total_elapsed / num_reps\n",
    "            print('Avg Run time: {0:.6f}s ({1} reps)'.format(avg_elapsed,\n",
    "                                                            num_reps))\n",
    "            return result\n",
    "        return inner\n",
    "    return decorator  "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "@timed(5)\n",
    "def fib(n):\n",
    "    return calc_fib_recurse(n)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Avg Run time: 0.253744s (5 reps)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "832040"
      ]
     },
     "execution_count": 36,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "fib(30)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.2"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
